﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ServiceModel;
using System.ServiceModel.Discovery;
using System.Timers;
using ServiceContract;
using System.Xml;
using System.Threading;
using EnOcean.Radio.Sensor;

namespace EnOceanTransmitter
{
  public class Transmitter
  {
    /// <summary>
    /// The service that receives announcement messages.
    /// </summary>
    private ServiceHost announcementServiceHost;

    /// <summary>
    /// Timer for pinging receiver services.
    /// </summary>
    private System.Timers.Timer pingTimer;
    private System.Timers.Timer discoveryTimer;
    private DiscoveryClient dc;
    private FindCriteria find;

    private readonly Dictionary<string, ActiveTransmitter> transmitters = new Dictionary<string, ActiveTransmitter>();
    private ReaderWriterLock transmittersLock = new ReaderWriterLock();

    public event EventHandler TransmitterAvailable = delegate { };
    public event EventHandler TransmitterUnavailable = delegate { };
    public event EventHandler<UnhandledExceptionEventArgs> UnhandledException = delegate { };

    public void Start()
    {
      AnnouncementService announce;

      pingTimer = new System.Timers.Timer(10000);
      pingTimer.Elapsed += new ElapsedEventHandler(PingTimer_Elapsed);
      pingTimer.AutoReset = true;
      pingTimer.Start();

      dc = new DiscoveryClient(new UdpDiscoveryEndpoint());
      dc.FindProgressChanged += new EventHandler<FindProgressChangedEventArgs>(FindProgressChanged);
      find = new FindCriteria(typeof(IEnOceanTransmitter)) { Duration = TimeSpan.FromSeconds(5) };

      discoveryTimer = new System.Timers.Timer(15000);
      discoveryTimer.Elapsed += new ElapsedEventHandler(DiscoveryTimer_Elapsed);
      discoveryTimer.AutoReset = true;
      discoveryTimer.Start();

      // Initialize the announcement service.
      announce = new AnnouncementService();
      announce.OnlineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(OnlineAnnouncementReceived);
      announce.OfflineAnnouncementReceived += new EventHandler<AnnouncementEventArgs>(OfflineAnnouncementReceived);
      announcementServiceHost = new ServiceHost(announce);
      announcementServiceHost.AddServiceEndpoint(new UdpAnnouncementEndpoint());
      announcementServiceHost.Open();

    }

    public void Stop()
    {
      pingTimer.Stop();
      pingTimer = null;

      discoveryTimer.Stop();
      discoveryTimer = null;
      dc.Close();

      announcementServiceHost.Close();
      announcementServiceHost = null;
    }

    public IEnumerable<long> GetAvailableBaseIds()
    {
      List<long> ids;

      ids = new List<long>();
      transmittersLock.AcquireReaderLock(-1);

      try
      {
        foreach (ActiveTransmitter at in transmitters.Values)
        {
          foreach (long id in at.BaseIds)
          {
            ids.Add(id);
          }
        }
      }
      finally
      {
        transmittersLock.ReleaseReaderLock();
      }

      return ids;
    }

    public void Send(SensorTelegram telegram)
    {
      bool sent;
      transmittersLock.AcquireReaderLock(-1);

      try
      {
        sent = false;

        foreach (ActiveTransmitter at in transmitters.Values)
        {
          foreach (long id in at.BaseIds)
          {
            if (id == (telegram.Id & 0xFFFFFF80))
            {
              at.TransmitterServiceProxy.SendTelegram(telegram.GetBytes());
              sent = true;
              break;
            }
          }
        }

        if (!sent)
        {
          throw new ArgumentOutOfRangeException("No transievers matched the given sensor ID.");
        }
      }
      finally
      {
        transmittersLock.ReleaseReaderLock();
      }
    }

    private void DiscoveryTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
      try
      {
        dc.FindAsync(find);
      }
      catch (Exception ex)
      {
        UnhandledException(this, new UnhandledExceptionEventArgs(ex, false));
      }
    }

    private void PingTimer_Elapsed(object sender, ElapsedEventArgs e)
    {
      List<string> failed;

      failed = new List<string>();
      transmittersLock.AcquireReaderLock(-1);

      try
      {
        foreach (ActiveTransmitter at in transmitters.Values)
        {
          try
          {
            at.TransmitterServiceProxy.SendTelegram(null); // Ping message
          }
          catch (Exception)
          {
            failed.Add(at.TransmitterServiceProxy.Endpoint.Address.ToString());
          }
        }
      }
      finally
      {
        transmittersLock.ReleaseReaderLock();
      }

      foreach (string key in failed)
      {
        RemoveTransmitter(key);
      }
    }

    private void FindProgressChanged(object sender, FindProgressChangedEventArgs e)
    {
      try
      {
        Console.WriteLine("FindProgressChanged");
        AddTransmitter(e.EndpointDiscoveryMetadata.Address);
      }
      catch (Exception ex)
      {
        UnhandledException(this, new UnhandledExceptionEventArgs(ex, false));
      }
    }

    private void OnlineAnnouncementReceived(object sender, AnnouncementEventArgs e)
    {
      try
      {
        foreach (XmlQualifiedName name in e.EndpointDiscoveryMetadata.ContractTypeNames)
        {
          if (!name.IsEmpty && name.Namespace == "http://enoceannet.codeplex.com/EnOceanTransmitter/v1" && name.Name == "IEnOceanTransmitter")
          {
            Console.WriteLine("OnlineAnnouncementReceived");
            AddTransmitter(e.EndpointDiscoveryMetadata.Address);
            break;
          }
        }
      }
      catch (Exception ex)
      {
        UnhandledException(this, new UnhandledExceptionEventArgs(ex, false));
      }
    }

    private void OfflineAnnouncementReceived(object sender, AnnouncementEventArgs e)
    {
      try
      {
        foreach (XmlQualifiedName name in e.EndpointDiscoveryMetadata.ContractTypeNames)
        {
          if (!name.IsEmpty && name.Namespace == "http://enoceannet.codeplex.com/EnOceanTransmitter/v1" && name.Name == "IEnOceanTransmitter")
          {
            Console.WriteLine("OfflineAnnouncementReceived");
            RemoveTransmitter(e.EndpointDiscoveryMetadata.Address.ToString());
            break;
          }
        }
      }
      catch (Exception ex)
      {
        UnhandledException(this, new UnhandledExceptionEventArgs(ex, false));
      }
    }

    private void AddTransmitter(EndpointAddress address)
    {
      ActiveTransmitter at;
      LockCookie lc;
      string key;
      IEnumerable<long> ids;
      bool added;

      key = address.ToString();
      added = false;

      try
      {
        transmittersLock.AcquireReaderLock(-1);

        if (!transmitters.ContainsKey(key))
        {
          at = new ActiveTransmitter();
          at.TransmitterServiceProxy = new TransmitterServiceProxy(address);
          at.BaseIds = at.TransmitterServiceProxy.GetBaseIds();

          lc = transmittersLock.UpgradeToWriterLock(-1);

          try
          {
            if (!transmitters.ContainsKey(key))
            {
              transmitters.Add(key, at);
              added = true;
            }
          }
          finally
          {
            transmittersLock.DowngradeFromWriterLock(ref lc);
          }
        }
        else
        {
          at = transmitters[key];
          lc = transmittersLock.UpgradeToWriterLock(-1);

          try
          {
            ids = at.TransmitterServiceProxy.GetBaseIds();

            if (!ids.SequenceEqual(at.BaseIds))
            {
              at.BaseIds = ids;
              added = true;
            }
          }
          finally
          {
            transmittersLock.DowngradeFromWriterLock(ref lc);
          }
        }
      }
      finally
      {
        transmittersLock.ReleaseReaderLock();
      }

      if (added)
      {
        TransmitterAvailable(this, new EventArgs());
      }

    }

    private void RemoveTransmitter(string key)
    {
      transmittersLock.AcquireWriterLock(-1);

      try
      {
        transmitters.Remove(key);
      }
      finally
      {
        transmittersLock.ReleaseWriterLock();
      }

      TransmitterUnavailable(this, new EventArgs());
    }
  }
}
